package cn.lijiajia3515.cairo.auth.modules.account;

import cn.lijiajia3515.cairo.auth.AuthConstants;
import cn.lijiajia3515.cairo.auth.domain.mongo.AccountMongo;
import cn.lijiajia3515.cairo.auth.domain.mongo.AuthMongoConstants;
import cn.lijiajia3515.cairo.auth.modules.AuthExtensionConstants;
import cn.lijiajia3515.cairo.auth.modules.AuthRabbitConstants;
import cn.lijiajia3515.cairo.auth.modules.department.DepartmentClient;
import cn.lijiajia3515.cairo.auth.modules.department.FindDepartmentParam;
import cn.lijiajia3515.cairo.auth.modules.department.PathDepartment;
import cn.lijiajia3515.cairo.auth.modules.role.FindRoleParam;
import cn.lijiajia3515.cairo.auth.modules.role.Role;
import cn.lijiajia3515.cairo.auth.modules.role.RoleClient;
import cn.lijiajia3515.cairo.auth.modules.role.RoleExtension;
import cn.lijiajia3515.cairo.auth.modules.serial.SerialClient;
import cn.lijiajia3515.cairo.common.BaseService;
import cn.lijiajia3515.cairo.core.CoreConstants;
import cn.lijiajia3515.cairo.core.exception.BusinessException;
import cn.lijiajia3515.cairo.core.exception.UnknownBusinessException;
import cn.lijiajia3515.cairo.core.page.Page;
import cn.lijiajia3515.cairo.mongo.data.LastModifiedMongo;
import cn.lijiajia3515.cairo.security.context.CairoSecurityContextHolder;
import com.mongodb.client.result.UpdateResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static cn.lijiajia3515.cairo.auth.modules.account.AccountConstants.DEFAULT_PASSWORD;
import static java.util.stream.Collectors.toMap;

/**
 * 账号 业务服务
 */
@Slf4j
@Service
public class AccountService extends BaseService {

	private final MongoTemplate mongoTemplate;
	private final RabbitTemplate rabbitTemplate;
	private final SerialClient serialClient;
	private final DepartmentClient departmentClient;
	private final RoleClient roleClient;
	private final PasswordEncoder passwordEncoder;

	public AccountService(MongoTemplate mongoTemplate,
						  AccountRClient userRClient,
						  RabbitTemplate rabbitTemplate,
						  PasswordEncoder passwordEncoder,
						  SerialClient serialClient,
						  RoleClient roleClient,
						  DepartmentClient departmentClient
						  ) {
		super(userRClient);
		this.mongoTemplate = mongoTemplate;
		this.rabbitTemplate = rabbitTemplate;
		this.passwordEncoder = passwordEncoder;
		this.serialClient = serialClient;
		this.departmentClient = departmentClient;
		this.roleClient = roleClient;
	}

	/**
	 * 注册 账号
	 *
	 * @param param 参数
	 * @return 账号信息
	 */
	@Transactional(rollbackFor = Exception.class)
	Account reg(@Validated(SaveAccountParam.Reg.class) SaveAccountParam param) {
		AccountMongo data = AccountMongo.builder()
			.id(newUid())
			.name(Strings.isEmpty(param.getName()) ? param.getUsername() : param.getName())
			.username(param.getUsername())
			.phoneNumber(param.getPhoneNumber())
			.email(param.getEmail())
			.password(passwordEncoder.encode(Optional.ofNullable(param.getPassword()).orElse(DEFAULT_PASSWORD)))
			.accountEnabled(false)
			.accountLocked(false)
			.department(Collections.emptyMap())
			.role(Collections.emptyMap())
			.resource(Collections.emptyMap())
			.build();
		AccountMongo insert = mongoTemplate.insert(data, AuthMongoConstants.Collection.ACCOUNT);
		log.debug("[user][reg] result -> {} ", insert);
		rabbitTemplate.convertAndSend(AuthRabbitConstants.Exchange.HFHK, AuthRabbitConstants.Key.USER_SAVE, SavedAccountMessage.builder().accountIds(Collections.singleton(insert.getId())).at(LocalDateTime.now()).build());
		return findById(null, insert.getId()).orElseThrow();
	}

	/**
	 * 后台 新增用户
	 *
	 * @param appId appId
	 * @param param  param
	 * @return user
	 */
	@Transactional(rollbackFor = Exception.class)
	Account save(@Validated @NotNull String appId, @Validated(SaveAccountParam.class) SaveAccountParam param) {
		if (existPhoneNumber(param.getPhoneNumber(), Collections.emptySet())) {
			throw new BusinessException(param.getPhoneNumber(), AccountBusiness.PhoneNumberExists);
		}

		AccountMongo data = AccountMongo.builder()
			.id(newUid())
			.name(Strings.isEmpty(param.getName()) ? param.getName() : param.getUsername())
			.username(param.getUsername())
			.phoneNumber(param.getPhoneNumber())
			.email(param.getEmail())
			.password(passwordEncoder.encode(Optional.ofNullable(param.getPassword()).orElse(DEFAULT_PASSWORD)))
			.accountEnabled(true)
			.accountLocked(false)
			.role(Collections.singletonMap(appId, param.getRoleIds()))
			.department(Collections.singletonMap(appId, param.getDepartmentIds()))
			.resource(Collections.singletonMap(appId, param.getResourceIds()))
			.build();

		AccountMongo insert = mongoTemplate.insert(data, AuthMongoConstants.Collection.ACCOUNT);
		rabbitTemplate.convertAndSend(AuthRabbitConstants.Exchange.HFHK, AuthRabbitConstants.Key.USER_SAVE, SavedAccountMessage.builder()
			.client(appId)
			.accountIds(Collections.singleton(insert.getId()))
			.at(LocalDateTime.now())
			.build()
		);
		log.debug("[user][save] result -> {} ", insert);
		return findById(appId, insert.getId()).orElseThrow();
	}

	/**
	 * 修改
	 *
	 * @param param param
	 * @return user
	 */
	@Transactional(rollbackFor = Exception.class)
	Optional<Account> modify(@NotNull String appId, @Validated ModifyAccountParam param) {
		if (existPhoneNumber(param.getPhoneNumber(), Collections.singleton(param.getUid()))) {
			throw new BusinessException(param.getPhoneNumber(), AccountBusiness.PhoneNumberExists);
		}
		Query query = Query.query(Criteria.where(AccountMongo.FIELD.ID).is(param.getUid()));
		Update update = new Update();
		Optional.ofNullable(param.getUsername()).filter(x -> !x.isBlank()).ifPresent(x -> update.set(AccountMongo.FIELD.USERNAME, x));
		Optional.ofNullable(param.getPhoneNumber()).filter(x -> !x.isBlank()).ifPresent(x -> update.set(AccountMongo.FIELD.PHONE_NUMBER, x));
		Optional.ofNullable(param.getEmail()).filter(x -> !x.isBlank()).ifPresent(x -> update.set(AccountMongo.FIELD.EMAIL, x));
		Optional.ofNullable(param.getName()).filter(x -> !x.isBlank()).ifPresent(x -> update.set(AccountMongo.FIELD.NAME, x));

		Optional.ofNullable(param.getRoleIds()).ifPresent(x -> update.set(AccountMongo.FIELD.ROLE.client(appId), x));
		Optional.ofNullable(param.getDepartmentIds()).ifPresent(x -> update.set(AccountMongo.FIELD.DEPARTMENT.client(appId), x));
		Optional.ofNullable(param.getResourceIds()).ifPresent(x -> update.set(AccountMongo.FIELD.RESOURCE.client(appId), x));

		UpdateResult updateResult = mongoTemplate.updateFirst(query, update, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT);

		// message
		rabbitTemplate.convertAndSend(AuthRabbitConstants.Exchange.HFHK, AuthRabbitConstants.Key.USER_SAVE, SavedAccountMessage.builder().client(appId).accountIds(Collections.singleton(param.getUid())).at(LocalDateTime.now()).build());
		log.debug("[user][modify] result -> {}", updateResult);

		return findUserByUid(appId, param.getUid());
	}

	/**
	 * 修改密码
	 *
	 * @param param param
	 */
	@Transactional(rollbackFor = Exception.class)
	void modifyPassword(@Valid @NotNull String appId, @Valid @NotNull String uid, @Valid ModifyPasswordAccountParam param) {
		String rawPassword = Optional.ofNullable(param.getPassword()).orElse(DEFAULT_PASSWORD);

		Query query = Query.query(Criteria.where(AccountMongo.FIELD.ID).is(uid));
		query.fields().include(AccountMongo.FIELD.PASSWORD);
		Optional.ofNullable(mongoTemplate.findOne(query, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT))
			.map(AccountMongo::getPassword)
			.filter(encodedPassword -> passwordEncoder.matches(rawPassword, encodedPassword))
			.orElseThrow(() -> new UnknownBusinessException("密码错误"));

		Update update = Update.update(AccountMongo.FIELD.PASSWORD, passwordEncoder.encode(param.getNewPassword()));

		UpdateResult updateResult = mongoTemplate.updateFirst(query, update, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT);

		// message
		rabbitTemplate.convertAndSend(AuthRabbitConstants.Exchange.HFHK, AuthRabbitConstants.Key.USER_SAVE, SavedAccountMessage.builder().client(appId).accountIds(Collections.singleton(uid)).at(LocalDateTime.now()).build());
		log.debug("[user][password modify] result -> {}", updateResult);

	}

	/**
	 * 密码重置
	 *
	 * @param param param
	 * @return 重置后的密码
	 */
	@Transactional(rollbackFor = Exception.class)
	Optional<String> resetPassword(@Valid @NotNull String appId, @Valid ResetPasswordAccountParam param) {
		String password = Optional.ofNullable(param.getPassword()).orElse(DEFAULT_PASSWORD);
		Query query = Query.query(Criteria.where(AccountMongo.FIELD.ID).is(param.getId()));
		Update update = Update.update(AccountMongo.FIELD.PASSWORD, passwordEncoder.encode(password));

		AccountMongo updateAccount = mongoTemplate.findAndModify(query, update, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT);
		log.debug("[account][password reset] result -> {}", updateAccount);

		// message
		rabbitTemplate.convertAndSend(AuthRabbitConstants.Exchange.HFHK, AuthRabbitConstants.Key.USER_SAVE, SavedAccountMessage.builder().client(appId).accountIds(Collections.singleton(param.getId())).at(LocalDateTime.now()).build());
		return Optional.ofNullable(updateAccount).map(x -> password);
	}

	/**
	 * 密码重置
	 *
	 * @param param param
	 * @return 重置后的密码
	 */
	@Transactional(rollbackFor = Exception.class)
	Optional<Boolean> modifyStatus(@NotNull String appId, @Validated ModifyStatusAccountParam param) {
		Criteria criteria = Criteria.where(AccountMongo.FIELD.ID).is(param.getUid());
		Query query = Query.query(criteria);
		Update update = Update.update(AccountMongo.FIELD.ACCOUNT_ENABLED, param.getStatus());

		AccountMongo updateUser = mongoTemplate.findAndModify(query, update, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT);
		log.debug("[user][modify status] result -> {}", updateUser);

		// message
		rabbitTemplate.convertAndSend(AuthRabbitConstants.Exchange.HFHK, AuthRabbitConstants.Key.USER_SAVE, SavedAccountMessage.builder().client(appId).accountIds(Collections.singleton(param.getUid())).at(LocalDateTime.now()).build());
		return Optional.ofNullable(updateUser).map(AccountMongo::getAccountEnabled);
	}


	Optional<Account> findUserByUid(@NotNull String appId, @NotNull String uid) {
		Query query = Query.query(Criteria.where(AccountMongo.FIELD.ID).is(uid));
		return Optional.ofNullable(mongoTemplate.findOne(query, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT)).flatMap(m -> find(Collections.singletonList(m), appId, Collections.emptyMap()).stream().findFirst());
	}


	/**
	 * find
	 *
	 * @param appId appId
	 * @param param  param
	 * @return user list
	 */
	List<Account> find(@Valid @NotNull String appId, @Valid @Validated FindAccountParam param) {
		Criteria criteria = buildFindParamCriteria(appId, param);
		Query query = new Query(criteria);

		List<AccountMongo> users = mongoTemplate.find(query, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT);
		log.debug("[user][find] query: {}", users);

		return find(users, appId, param.getExtension());
	}


	/**
	 * find page
	 *
	 * @return user page
	 */
	Page<Account> findPage(@NotNull String appId, @Validated FindAccountParam param) {
		Criteria criteria = buildFindParamCriteria(appId, param);
		Query query = new Query(criteria);

		long total = mongoTemplate.count(query, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT);

		query.with(param.pageable());
		List<AccountMongo> ms = mongoTemplate.find(query, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT);
		log.debug("[user][pageFind] query: {}", ms);

		List<Account> contents = find(ms, appId, param.getExtension());

		return new Page<>(param, contents, total);
	}

	/**
	 * 当前用户自行修改
	 *
	 * @param param 参数
	 */
	void modify(@Valid @NotNull String appId, @Validated ModifyCurrentAccountParam param) {
		Criteria criteria = Criteria
			.where(AccountMongo.FIELD.META.DELETED).is(AuthMongoConstants.NO_DELETED_VALUE)
			.and(AccountMongo.FIELD.ID).is(CairoSecurityContextHolder.authenticationAccountId());


		Update update = Update.update(AccountMongo.FIELD.META.LAST_MODIFIED.SELF, LastModifiedMongo.builder()
			.accountId(CairoSecurityContextHolder.authenticationAccountId())
			.at(LocalDateTime.now())
			.build());
		Optional.ofNullable(param.getAvatarUrl()).ifPresent(x -> update.set(AccountMongo.FIELD.AVATAR_URL, x));
		Optional.ofNullable(param.getName()).ifPresent(x -> update.set(AccountMongo.FIELD.NAME, x));

		final UpdateResult updateResult = mongoTemplate.update(AccountMongo.class)
			.inCollection(AuthMongoConstants.Collection.ACCOUNT)
			.matching(criteria)
			.apply(update)
			.first();
	}

	/**
	 * find by id
	 *
	 * @param appId appId
	 * @param uid    uid
	 * @return user
	 */
	Optional<Account> findById(String appId, String uid) {
		Criteria criteria = Criteria.where(AccountMongo.FIELD.ID).is(uid);
		Query query = Query.query(criteria);

		return Optional.ofNullable(mongoTemplate.findOne(query, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT))
			.flatMap(m -> find(Collections.singletonList(m), appId, Collections.emptyMap()).stream().findFirst());
	}

	List<Account> find(List<AccountMongo> ms, String client, Map<String, String> extensionMap) {
		AccountExtension extension = Optional.ofNullable(extensionMap.get(AuthExtensionConstants.USER)).map(AccountExtension::valueOf).orElse(AccountExtension.ALL);
		final Map<String, Account> accountMap = Optional.of(extension.fields())
			.filter(x -> x.contains(AccountField.META))
			.map(x -> getMetaAccountIds(ms.stream().map(AccountMongo::getMeta).collect(Collectors.toSet())))
			.map(x -> {
				final Criteria criteria = Criteria
					.where(AccountMongo.FIELD.META.DELETED).is(AuthMongoConstants.NO_DELETED_VALUE)
					.and(AccountMongo.FIELD.ID).in(x);
				Query query = Query.query(criteria);
				return mongoTemplate.find(query, AccountMongo.class, AuthMongoConstants.Collection.ACCOUNT)
					.stream()
					.map(z -> AccountConverter.convert(client, z, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), AccountExtension.BASIC))
					.collect(toMap(Account::getId, z -> z));
			})
			.orElse(Collections.emptyMap());

		Map<String, Role> roleMap = Optional.of(extension.fields())
			.filter(x -> x.contains(AccountField.ROLE))
			.map(x -> FindRoleParam.builder()
				.ids(roleIdsMapper(client, ms))
				.extension(new HashMap<>() {{
					put(AuthExtensionConstants.ROLE, RoleExtension.BASIC.name());
				}})
				.build()
			)
			.map(x -> roleClient.find(x).stream().collect(Collectors.toMap(Role::getId, z -> z)))
			.orElse(Collections.emptyMap());

		Map<String, PathDepartment> departmentMap = Optional.of(extension.fields())
			.filter(x -> x.contains(AccountField.DEPARTMENT))
			.map(x -> FindDepartmentParam.builder().ids(departmentIdsMapper(client, ms)).build())
			.map(x -> departmentClient.findAncestor(x).stream().collect(Collectors.toMap(d -> d.getIds().get(d.getIds().size() - 1), d -> d)))
			.orElse(Collections.emptyMap());

		return ms.stream().map(m -> AccountConverter.convert(client, m, roleMap, departmentMap, accountMap, extension)).collect(Collectors.toList());
	}


	@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
	boolean existPhoneNumber(String phoneNumber, Set<String> ignoreUids) {
		final Criteria criteria = Criteria
			.where(AccountMongo.FIELD.META.DELETED).is(AuthMongoConstants.NO_DELETED_VALUE)
			.and(AccountMongo.FIELD.PHONE_NUMBER).is(phoneNumber);
		Optional.ofNullable(ignoreUids).filter(x -> !x.isEmpty()).ifPresent(x -> criteria.and(AccountMongo.FIELD.ID).nin(ignoreUids));
		return mongoTemplate.exists(Query.query(criteria), AuthMongoConstants.Collection.ACCOUNT);
	}

	String newUid() {
		String id;
		try {
			id = serialClient.next(AuthConstants.CLIENT, AccountConstants.SERIAL).toString();
		} catch (Exception e) {
			id = CoreConstants.SNOWFLAKE.nextIdStr();
		}
		return "U".concat(id);

	}

	Set<String> roleIdsMapper(String appId, List<AccountMongo> users) {
		return users.stream().flatMap(x -> Optional.ofNullable(x.getRole()).stream())
			.filter(clientRole -> clientRole.containsKey(appId))
			.flatMap(clientRole -> Optional.ofNullable(clientRole.get(appId)).stream().flatMap(Collection::stream))
			.collect(Collectors.toSet());
	}

	Set<String> departmentIdsMapper(String appId, List<AccountMongo> users) {
		return users.stream().flatMap(x -> Optional.ofNullable(x.getDepartment()).stream())
			.filter(clientDepartments -> clientDepartments.containsKey(appId))
			.flatMap(clientDepartments -> Optional.ofNullable(clientDepartments.get(appId)).stream().flatMap(Collection::stream))
			.collect(Collectors.toSet());
	}

	Criteria buildFindParamCriteria(String appId, FindAccountParam param) {
		Criteria criteria = new Criteria();
		Optional.ofNullable(param.getKeyword()).filter(x -> !x.isEmpty())
			.map(x -> new Criteria[]{
				Criteria.where(AccountMongo.FIELD.ID).regex(x),
				Criteria.where(AccountMongo.FIELD.USERNAME).regex(x),
				Criteria.where(AccountMongo.FIELD.PHONE_NUMBER).regex(x),
				Criteria.where(AccountMongo.FIELD.EMAIL).regex(x),
				Criteria.where(AccountMongo.FIELD.NAME).regex(x)
			}).ifPresent(criteria::orOperator);

		Optional.ofNullable(param.getUids()).filter(x -> !x.isEmpty()).ifPresent(x -> criteria.and(AccountMongo.FIELD.ID).in(x));
		Optional.ofNullable(param.getRoleIds()).filter(x -> !x.isEmpty()).ifPresent(x -> criteria.and(AccountMongo.FIELD.ROLE.client(appId)).in(x));
		Optional.ofNullable(param.getStatuses()).filter(x -> !x.isEmpty()).map(x -> x.stream().filter(Objects::nonNull).collect(Collectors.toList())).ifPresent(x -> criteria.and(AccountMongo.FIELD.ACCOUNT_ENABLED).in(x));
		return criteria;
	}
}
